﻿
using Boilen.Primitives.Members;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Xml.Linq;


namespace Boilen.Primitives.Implementers {

    /// <summary>
    /// Implements the <see cref="Freezable"/> base class and the user-defined silverlight <c>IFreezable</c> interface.
    /// </summary>
    public sealed class FreezableImplementer : InterfaceImplementer<Freezable> {

        public const string FreezableInterfaceName = "IFreezable";

        public const string SafeFreezeHelperName = "SafeFreeze";

        public const string OnFreezeName = "OnFreeze";

        public const string FreezePropertiesName = "FreezeProperties";

        private const string SubpropertyChangedEventName = "SubpropertyChanged";

        private static readonly AttributeMember EmptyFreezePropertiesBodyAttribute =
            AttributeMember.SuppressMessage( "Microsoft.Performance", "CA1822:MarkMembersAsStatic", "Implemented as part of SL Freezable pattern." );

        private static readonly AttributeMember OnEventAttribute =
            AttributeMember.SuppressMessage( "Microsoft.Design", "CA1030:UseEventsWhereAppropriate", "Supports the SubpropertyChanged event." );


        /// <summary>
        /// Gets or sets a value indicating whether <see cref="Freezable"/> members should be declared or inherited from the base type.
        /// </summary>
        public bool DeclareMembers { get; set; }


        /// <inheritdoc/>
        protected override bool CreateGroup { get { return false; } }

        /// <inheritdoc/>
        protected override IEnumerable<InheritanceMember> ImplementedInterfaces {
            get {
                if( this.DeclareMembers )
                    yield return new InheritanceMember( FreezableInterfaceName, Extensions.Freezable ) {
                        Condition = CompilationSymbol.Silverlight
                    };
            }
        }

        /// <inheritdoc/>
        protected override IEnumerable<Member> InterfaceMembers {
            get {
                var freezablePropertyNames = this.GetTargetData( ( ITarget t ) => t.Freezable, ( ITarget t ) => t.PropertyName );
                var slFreezableMembers = new MemberGroup( FreezableInterfaceName + " Members" ) { Condition = CompilationSymbol.Silverlight };

                // Freezable:
                // protected override Freezable CreateInstanceCore();
                if( !this.Parent.Type.IsAbstract ) {
                    var createInstanceCoreMethod = CreateCreateInstanceCoreMethod( );
                    yield return new MemberGroup( "Freezable Members" ) {
                        Condition = CompilationSymbol.NotSilverlight,
                        Members = { createInstanceCoreMethod }
                    };
                }

                if( this.DeclareMembers ) {
                    // IFreezable:
                    string isFrozenFieldName = Property<bool>.PropertyFieldNamePrefix + "IsFrozen";
                    string isFrozenFieldType = this.Parent.TypeRepository.GetTypeName( typeof( bool ) );
                    string subpropertyChangedFieldName = Event<EventHandler>.EventFieldNamePrefix + SubpropertyChangedEventName;
                    string subpropertyChangedFieldType = this.Parent.TypeRepository.GetTypeName( typeof( EventHandler ) );

                    // public bool IsFrozen { get; }
                    slFreezableMembers.Members.Add( new FieldMember( isFrozenFieldName, isFrozenFieldType ) );
                    slFreezableMembers.Members.Add( this.CreateIsFrozenProperty( isFrozenFieldName ) );

                    // public void Freeze();
                    slFreezableMembers.Members.Add( this.CreateFreezeMethod( isFrozenFieldName, subpropertyChangedFieldName ) );

                    // partial void OnFreeze();
                    slFreezableMembers.Members.Add( this.CreateOnFreezeMethod( ) );

                    // protected virtual FreezeProperties();
                    slFreezableMembers.Members.Add( this.CreateFreezePropertiesMethod( true, freezablePropertyNames ) );

                    // event EventHandler IFreezable.SubpropertyChanged;
                    slFreezableMembers.Members.Add( new FieldMember( subpropertyChangedFieldName, subpropertyChangedFieldType ) );
                    slFreezableMembers.Members.Add( this.CreateSubpropertyChangedEvent( subpropertyChangedFieldName, subpropertyChangedFieldType ) );
                    slFreezableMembers.Members.Add( this.CreateOnSubpropertyChangedMethod( subpropertyChangedFieldName, subpropertyChangedFieldType ) );
                }
                else {
                    // IFreezable:
                    // protected override FreezeProperties();
                    slFreezableMembers.Members.Add( this.CreateFreezePropertiesMethod( false, freezablePropertyNames ) );
                }

                yield return slFreezableMembers;
            }
        }


        /// <inheritdoc/>
        public FreezableImplementer( PartialType parent )
            : base( parent ) { }


        /// <inheritdoc/>
        public override void Prepare( ) {
            base.Prepare( );
            this.PrepareTargets( ( ITarget t ) => t.Prepare( this ) );
        }


        private static IEnumerable<XElement> GetMemberDoc( MemberInfo member ) {
            var memberDoc = XmlDocumentation.GetDocMember( member );
            return XmlDocumentation.FilterElements( memberDoc, "exception" );
        }

        private MethodMember CreateCreateInstanceCoreMethod( ) {
            Doc doc = this.CreateDoc( );
            doc.InheritFrom = "System.Windows.Freezable.CreateInstanceCore";

            var createInstanceCoreBody = new BlockMember( "CreateInstanceCore", w => w.WriteLine( "return new {0}();", this.Parent.TypeName ) );

            var createInstanceCoreMethod = new MethodMember( "CreateInstanceCore", "Freezable", createInstanceCoreBody ) {
                Modifiers = "protected override",
                Doc = doc
            };

            return createInstanceCoreMethod;
        }

        private AccessorMember CreateIsFrozenProperty( string fieldName ) {
            var freezableIsFrozenProperty = typeof( Freezable ).GetProperty( "IsFrozen" );

            string propertyName = freezableIsFrozenProperty.Name;
            string propertyType = this.Parent.TypeRepository.GetTypeName( freezableIsFrozenProperty.PropertyType );

            return new AccessorMember( propertyName, propertyType ) {
                ObserveMember = new BlockMember( "get", string.Format( "return this.{0};", fieldName ) ) { WriteName = true },
                Doc = this.CreateDoc( )
                    .AddDocElements( GetMemberDoc( freezableIsFrozenProperty ) )
            };
        }

        private MethodMember CreateFreezeMethod( string isFrozenFieldName, string subpropertyChangedFieldName ) {
            var freezableFreezeMethod = typeof( Freezable ).GetMethod( "Freeze", Type.EmptyTypes );

            string methodName = freezableFreezeMethod.Name;
            string returnType = this.Parent.TypeRepository.GetTypeName( freezableFreezeMethod.ReturnType );

            var freezeMethodBody = new BlockMember( methodName, w => {
                w.WriteLine( "if (!this.IsFrozen)" );
                using( Enclose.Braces( w ) ) {
                    w.WriteLine( "this.{0}();", OnFreezeName );
                    w.WriteLine( "this.{0}();", FreezePropertiesName );
                    w.WriteLine( "this.{0} = null;", subpropertyChangedFieldName );
                    w.WriteLine( "this.{0} = true;", isFrozenFieldName );
                }
            } );

            return new MethodMember( methodName, returnType, freezeMethodBody ) {
                Doc = this.CreateDoc( )
                    .AddDocElements( GetMemberDoc( freezableFreezeMethod ) )
                    .AddReplacement( "System.Windows.Freezable.", "" )
            };
        }

        private MethodMember CreateOnFreezeMethod( ) {
            return new MethodMember( OnFreezeName, "void", null );
        }

        private MethodMember CreateFreezePropertiesMethod( bool declare, IEnumerable<string> freezableProperties ) {
            string modifiers = declare && this.Parent.IsSealed
                ? "private"
                : "protected " + (declare ? "virtual" : "override");
            string methodName = FreezePropertiesName;
            string returnType = this.Parent.TypeRepository.GetTypeName( typeof( void ) );

            var freezePropertiesMethodBody = new BlockMember( methodName, w => {
                if( !declare )
                    w.WriteLine( "base.{0}();", methodName );

                foreach( string property in freezableProperties ) {
                    w.WriteLine( "this.{0}.{1}();", property, SafeFreezeHelperName );
                }
            } );

            var freezePropertiesMethod = new MethodMember( methodName, returnType, freezePropertiesMethodBody ) {
                Modifiers = modifiers,
                Doc = this.CreateDoc( )
                    .AddSummary( "Calls %see:{0}.Freeze% on every freezable property.", FreezableInterfaceName )
            };

            if( freezableProperties.Any( ) )
                freezePropertiesMethodBody.AddGuards( new[] { Guard.Empty } );
            else if( declare )
                freezePropertiesMethod.AddAttributes( new[] { EmptyFreezePropertiesBodyAttribute } );

            return freezePropertiesMethod;
        }

        private AccessorMember CreateSubpropertyChangedEvent( string fieldName, string fieldType ) {
            string eventName = FreezableInterfaceName + "." + SubpropertyChangedEventName;
            string bodyFormat = "if (!this.IsFrozen) {{ this.{0} {1}= value; }}";
            string addBody = string.Format( bodyFormat, fieldName, "+" );
            string removeBody = string.Format( bodyFormat, fieldName, "-" );

            return new AccessorMember( eventName, fieldType ) {
                Modifiers = "event",
                ObserveMember = new BlockMember( "add", addBody ) { WriteName = true },
                UpdateMember = new BlockMember( "remove", removeBody ) { WriteName = true },
                Doc = this.CreateDoc( )
                    .AddSummary( "Occurs when a property changes." )
            };
        }

        private MethodMember CreateOnSubpropertyChangedMethod( string fieldName, string fieldType ) {
            string onName = "On" + SubpropertyChangedEventName;
            string modifiers = this.Parent.IsSealed ? "private" : "protected";

            var onBody = new BlockMember( onName, w => {
                const string HandlerName = "handler";
                w.WriteLine( "{0} {1} = this.{2};", fieldType, HandlerName, fieldName );
                w.WriteLine( "if (!object.ReferenceEquals({0}, null))", HandlerName );
                using( Enclose.Braces( w ) ) { w.WriteLine( "{0}(this, EventArgs.Empty);", HandlerName ); }
            } );

            return new MethodMember( onName, "void", onBody ) {
                Modifiers = modifiers,
                Doc = this.CreateDoc( )
                   .AddSummary( "Raises the {0} event.", SubpropertyChangedEventName ),
                Attributes = { OnEventAttribute }
            };
        }


        /// <summary>
        /// Represents an <see cref="IImplementer"/> that supports the <see cref="Freezable"/> base class.
        /// </summary>
        public interface ITarget : IImplementer {
            /// <summary>
            /// Gets a value indicating whether a property type is freezable.
            /// </summary>
            bool Freezable { get; }

            /// <summary>
            /// Gets the name of the freezable property represented by the target.
            /// </summary>
            string PropertyName { get; }

            /// <summary>
            /// Called by <see cref="FreezableImplementer"/> to prepare a target implementer for the interface.
            /// </summary>
            void Prepare( FreezableImplementer implementer );
        }

    }

}
